home *** CD-ROM | disk | FTP | other *** search
-
- This project started as a challenge to make a friend's
- calculator program load and remain resident in memory on an IBM
- PC. Making a program written in assembly langauge stay resident
- has been presented in many articles and books, but writing the
- tools to make a C program resident was a new adventure. I
- developed all the examples in this article with Lattice's C
- Compiler version 3.0 and Microsoft's Macro Assembler 4.0. I have
- tried to make everything as portable as possible, but I'm sure
- that some modification will have to be made for different
- compilers and languages. In the listings, I have noted any
- compiler-dependent variables. (Editor's note: William Claff's
- article, "xxxxxx" on page ?? contains additional information on
- the topic of DOS extension via memory-resident programs.)
-
- WHAT IS A RESIDENT PROGRAM?
-
- DOS uses a set of pointers called Storage Blocks to keep
- track of allocated and unallocated memory in the system. For each
- loaded program, these pointers indicate the address its PSP
- (Program Segment Prefix) the program's length in segments. There
- is also a flag that indicates whether or not the memory pointed to
- by the Storage Block is allocated. When a program module is
- loaded and executes an INT 27H (terminate but stay resident) or
- DOS function 31H (keep process), COMMAND.COM makes sure that this
- program becomes a part of DOS. This means that the Storage Block,
- PSP, and the program module remain in memory and are not
- reallocated.
-
- The principles behind making a program resident seems to be
- straightforward, just find the length of the program, shove it
- into a register and call a documented function. DOS Function 31H
- requests the program size in paragraphs be placed in DX and the
- return code if any in AL.
-
- As demonstrated by the program shown in listing 1, it is a
- simple matter to make a program resident. If you have a utility
- like Norton's SI or SMAP you can verify that the program is indeed
- resident by looking at the location of the next program to be
- loaded address. You can also examine the amount of free memory
- displayed by the CHKDSK utility before and after running the
- program.
-
- Usually, we want to write a program that is more helpful than
- just taking up memory. Specifically, we want to write a program
- that responds to a system interrupt, and in doing so, supplies us
- with some sort of information. It should also be "well behaved"
- and operate within the constraints of DOS.
-
- The design of this installation system had several primary
- goals:
-
- * Modular design for universal application.
- * Optimum memory usage.
- * Correct processing of interrupts.
-
- Modular design means that I can, with minor revision, make
- this program load any module that meets with the requirements for
- a resident, interrupt processing program. To determine these
- requirements, I made a careful analysis of what my compiler did to
- a program, and what my linker did to the object modules supplied
- to it. If you are using a compiler and/or linker other than the
- ones I used, these requirements may be different. Listing 2 is an
- example of a completed sample system. Since we have little
- control right now over anything that happens above main(), we'll
- start there and analyze what happens. Refer to listing 3, which
- is a dissasembled version of the top-level code in listing 2.
-
- Cpush() and cpop() are two routines we'll create later to
- help us get into and return from the interrupts. Since main() is
- really just another function called by the compiler's entry module
- which is what is really loaded by EXEC, the BP register is saved
- and then set to the new SP. This is a requirement of any
- functions called from another routine that might pass any
- information on the stack; it allows the functions to reference
- that information on the stack via the BP register while still
- permitting new data to be pushed on the stack as required.
-
- Entry into a resident program should be designed so any
- parameters are passed in the DOS communications area or in
- registers, and not on the stack. Also, once a program module is
- installed in memory, we want to ignore the call to install().
- Although this uses six bytes of memory, passing the address of the
- call to cpush() to the interrupt vector is the most efficient way
- to install the module. All function names are made common in a C
- compiler so we can create the new vector IP by:
-
- nu_entry = (short)main + 6;
-
- Casting main to a short keeps it consistent with the way the
- rest of the register structures are typed. nu_entry now points to
- the desired entry point in the program. Since we did not need to
- use the compiler-generated PUSH BP, and we are returning from an
- interrupt we can ignore the POP BP and the RET that the compiler
- put at the end of main.
-
- The install() function is straightforward. In this example I
- borrowed an unused function call's vector to leave a signature or
- message to the calling program that we are already installed. To
- increase the safety of this routine, you could verify that the
- interrupt vector is filled with zeros first. If it is not, check
- another vector until one is found with no vector already
- installed. Alternately, you could indicate that the module is
- already installed by setting a flag in memory, but you would have
- to choose a byte that you are certain would not be used by some
- other routine.
-
- Another method for routines that handle passed values (i.e.
- video calls, put and get char and string calls) would be to detect
- a certain value, and return an 'already installed' message to the
- installation program. Listing 4 shows a segment of code that you
- could modify to perform this method of signature detection.
-
- The next task is to decide how to best utilize the memory
- taken up by the program. Since I used function call 31h instead
- of int 21h to terminate the program, loaded programs can exceed
- the 64k limit imposed by the latter. I can use .EXE programs with
- stack and data segments defined -- not just .COM programs. A .COM
- program uses as much memory as the machine has left when it is
- loaded; if the program is going to stay resident, it has to return
- its unused memory to the system.
-
- I release the memory that contains the program's copy of the
- environment using routine d_env(). On entry, the ES and DS (and
- SS and CS for a .COM program) segment registers point to the PSP
- at offset 0. Listing 5 shows the code for d_env(). I load ES
- with the address of the segment containing the copy of the
- environment and call DOS function 49H (free allocated memory).
-
- If the program is a .COM file, you can reduce its size using
- routine shrink() (see listing 6). This function sets the memory
- used by a program to the size of the program module in paragraphs.
- If you write .COM programs, be sure that you allocate stack area
- before calling this function. If you use shrink(), you should
- call it before calling d_env() so that the ES register contains
- the correct information for the call to function 4AH (modify
- allocated memory blocks). (You could modify the code to perform
- both operations with one call to increase the speed and reduce the
- size of the program.)
-
- The last area I will cover concerning memory management is
- one that is heavily influenced with my familiarity with the
- Lattice compiler. This compiler uses a file to set up the segment
- registers, handle stack and memory allocations, report errors such
- as stack overflows, handle command line arguments to change the
- stack size, redirect I/O, and some other incidental operations.
- The code for all this is found in the c.asm file -- its object
- module is in c.obj. This code is loaded before main() and cannot
- be efficiently deallocated by any means other than actually
- editing out unused portions of c.asm and recompiling the file. A
- knowledgeable programmer should be able to remove large portions
- of c.asm for many applications; I have reduced considerable space
- in mine.
-
- INTERRUPTS
-
- Now that I've shown how to load programs into memory and keep
- them resident, let's examine the available methods of processing
- the interrupts (keyboard, clock, etc.) and determine the best
- possible way to maintain 'nice' programs. My main concern is with
- the saving of registers and flags because of the amount of calls
- and subroutines normally found in a program written in C. (You
- can see an example of this in listing 1.)
-
- Since we passed the address of cpush() to the interrupt
- vector, the first thing the program does when it is entered is a
- call to cpush(). This call pushes a return address on the stack,
- one that would not be there if the code was being generated in
- assembly language. This problem is repeated throughout the
- program so it must be handled very early on.
-
- The three modules in listing 7 show one of the fastest and
- most efficient solutions I found. Upon entry into the program I
- call cpush(). This routine stores the short call return address,
- the interrupting program's return CS and IP, and the FLAGS that
- are pushed on the stack. It then stores the registers and segment
- registers in its own allocated memory. The short return address
- is then pushed back onto the stack and the function returns to the
- body of the program.
-
- After the interrupt routine does its work (in the example
- given in listing 2, it prints "Hello world"), it calls cpop() to
- return to the interrupted program. The cpop() routine emulates a
- pop of all the registers that should have been pushed onto the
- stack upon entry into the interrupt handler, and then does an
- IRET. (For debugging purposes, I have also included the code for
- cpopt(), which is similar to cpop() except that cpopt() exits via
- a RET instruction.)
-
- SUMMARY
-
- This article demonstrates a very simple interrupt processing
- program that remains resident in memory. I plan to do more work
- in writing programs that process the keyboard and video
- interrupts. Any programs written that use these techniques should
- be written with proper attention to good program structure, and
- correct manipulation of pointers and addresses. This project
- turned out to be a lot more ambitious than I originally thought.
- The entry and exit routines posed the most problem, testing and
- debugging sometimes left the machine in a very corrupted state.
- Be certain that your C programs can pass lint before using them;
- remember, you are creating a extension of DOS. I did notice that
- including structures in a program compiled with Lattice increased
- the address of the entry point by three. It makes a call after
- main() to set up the memory for the structs and/or unions. I will
- be interested in feedback about improving any of these algorithms
- and techniques.
-
-